-
Notifications
You must be signed in to change notification settings - Fork 403
fix mask editor bug under vueNodes #5953
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
🎨 Storybook Build Status✅ Build completed successfully! ⏰ Completed at: 10/13/2025, 01:40:41 AM UTC 🔗 Links🎉 Your Storybook is ready for review! |
🎭 Playwright Test Results⏰ Completed at: 10/13/2025, 01:55:22 AM UTC 📈 Summary
📊 Test Reports by Browser
🎉 Click on the links above to view detailed test results for each browser configuration. |
|
Yes this PR fixed the #5816 Screen.Recording.2025-10-07.at.18.06.18.mov |
| if (node) { | ||
| node.imageIndex = currentIndex.value | ||
| node.imgs = await Promise.all( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[quality] medium Priority
Issue: Promise rejection not handled - Image loading could fail silently
Context: If an image fails to load in the Promise.all, the entire operation fails but there's no error handling
Suggestion: Add proper error handling with try-catch or Promise.allSettled to handle individual failures gracefully
| if (node) { | ||
| node.imageIndex = currentIndex.value | ||
| node.imgs = await Promise.all( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[performance] medium Priority
Issue: Blocking image loading may freeze UI
Context: Promise.all waits for all images to load synchronously, potentially causing UI freezes with large images or slow networks
Suggestion: Consider loading images asynchronously or showing a loading indicator during this operation
| if (props.nodeId) { | ||
| const node = app.rootGraph?.getNodeById(props.nodeId) | ||
| if (node) { | ||
| node.imageIndex = currentIndex.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[architecture] medium Priority
Issue: Direct mutation of node object properties
Context: Directly setting node.imageIndex and node.imgs bypasses Vue reactivity and violates immutability principles
Suggestion: Use a reactive store or emit events to parent components to handle state changes properly
| if (ComfyApp.clipspace.imgs && node.imgs) { | ||
| if (node.images && ComfyApp.clipspace.images) { | ||
| // Update node.images even if it's initially undefined (vueNodes mode) | ||
| if (ComfyApp.clipspace.images) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[quality] high Priority
Issue: Logic condition removed may cause null reference errors
Context: Removed check for node.images existence before accessing ClipSpace.images could cause undefined/null references
Suggestion: Restore defensive check or add null safety: if (ComfyApp.clipspace.images && node.images)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need to remove this confition as under VueNodes, node.images will be undefined and need to be updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comprehensive PR Review
This review is generated by Claude. It may not always be accurate, as with human reviewers. If you believe that any of the comments are invalid or incorrect, please state why for each. For others, please implement the changes in one way or another.
Review Summary
PR: fix mask editor bug under vueNodes (#5953)
Impact: 39 additions, 4 deletions across 2 files
Issue Distribution
- Critical: 0
- High: 1
- Medium: 4
- Low: 1
Category Breakdown
- Architecture: 1 issue
- Security: 1 issue
- Performance: 1 issue
- Code Quality: 3 issues
Key Findings
Architecture & Design
The PR introduces direct state mutations on node objects, which bypasses Vue's reactivity system. This pattern could lead to inconsistent state management. Consider using proper reactive stores or event-driven communication instead.
Security Considerations
The addition of crossOrigin = 'anonymous' enables cross-origin access to images. While likely necessary for canvas operations, ensure this doesn't expose sensitive images to unauthorized access.
Performance Impact
The synchronous Promise.all for image loading could block the UI thread, especially with large images or slow networks. Consider asynchronous loading patterns or loading indicators.
Integration Points
The changes improve vueNodes integration with the mask editor by properly setting up node state and synchronizing with the node output store. However, removed safety checks could introduce runtime errors.
Positive Observations
- Good use of TypeScript with proper type annotations
- Proper async/await pattern for the mask editor function
- Icon updates follow the project's icon naming conventions (i-lucide:* format)
- Added proper fallback logic for vueNodes mode
References
Next Steps
- Address the high-priority logic issue in app.ts to prevent potential null reference errors
- Consider architectural feedback for proper state management patterns
- Add error handling for image loading operations
- Consider performance optimizations for image loading
- Validate CORS security implications
This is a comprehensive automated review. For architectural decisions requiring human judgment, please request additional manual review.
|
Accessing nodeOutputs by nodeId instead of nodeLocatorId will cause the code to break when run inside of subgraphs. But from looking into it, it's a decent amount of work to cleanup the surrounding code to use locatorIds instead of assuming the node resides in the active graph which is kinda out of scope. I can handle it in a separate PR after this one is merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the fundamental issue is that, when in Litegraph, both LoadImage and SaveImage have their imgs property assigned when their preview/output is udpated. For SaveImage it is set via this code
ComfyUI_frontend/src/composables/node/useNodeImage.ts
Lines 101 to 116 in 3f84bb0
| export const useNodeImage = (node: LGraphNode, callback?: () => void) => { | |
| node.previewMediaType = 'image' | |
| const loadElement = (url: string): Promise<HTMLImageElement | null> => | |
| new Promise((resolve) => { | |
| const img = new Image() | |
| img.onload = () => resolve(img) | |
| img.onerror = () => resolve(null) | |
| img.src = url | |
| }) | |
| const onLoaded = (elements: HTMLImageElement[]) => { | |
| node.imageIndex = null | |
| node.imgs = elements | |
| callback?.() | |
| } |
And useNodeImage is invoked in the draw loop here
ComfyUI_frontend/src/services/litegraphService.ts
Lines 872 to 896 in 3f84bb0
| const output = nodeOutputStore.getNodeOutputs(this) | |
| const preview = nodeOutputStore.getNodePreviews(this) | |
| const isNewOutput = output && this.images !== output.images | |
| const isNewPreview = preview && this.preview !== preview | |
| if (isNewPreview) this.preview = preview | |
| if (isNewOutput) this.images = output.images | |
| if (isNewOutput || isNewPreview) { | |
| this.animatedImages = output?.animated?.find(Boolean) | |
| const isAnimatedWebp = | |
| this.animatedImages && | |
| output?.images?.some((img) => img.filename?.includes('webp')) | |
| const isAnimatedPng = | |
| this.animatedImages && | |
| output?.images?.some((img) => img.filename?.includes('png')) | |
| const isVideo = | |
| (this.animatedImages && !isAnimatedWebp && !isAnimatedPng) || | |
| isVideoNode(this) | |
| if (isVideo) { | |
| useNodeVideo(this, callback).showPreview() | |
| } else { | |
| useNodeImage(this, callback).showPreview() |
But for LoadImage, I guess it is set in the useNodeImageUpload widget logic (which Vue nodes don't have). So, in Vue mode, only the SaveImage is getting node.imgs set. Then, the solution in this PR involves getting the image, initializing the Image object, then setting it manually.
A few questions:
- Should this logic go into its own function? Being a side-effect of
handleEditMaskis fairly confusing and I don't think future maintainers will understand the logic and what is happening - Do we need to create a new
Imageobject? Aren't we doubling the work? Can we not just do this:
<template>
<img
v-for="(url, i) in imageUrls"
:key="i"
:src="url"
ref="imageEls"
class="..."
/>
</template>
<script setup lang="ts">
const imageEls = ref<HTMLImageElement[]>([])
const handleEditMask = async () => {
if (!props.nodeId) return
const node = app.rootGraph?.getNodeById(props.nodeId)
if (!node) return
node.imageIndex = currentIndex.value
node.imgs = imageEls.value // ← already HTMLImageElements
app.canvas?.selectNode(node)
await commandStore.execute('Comfy.MaskEditor.OpenMaskEditor')
}
</script>7ee4899 to
74adabf
Compare
e141dd3 to
bee09d5
Compare
| if (!node) return | ||
| node.imageIndex = currentIndex.value | ||
| node.imgs = [currentImageEl.value] | ||
| app.canvas?.select(node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is necessary? How can the node be unselected if this is being called?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is necessary, please check the screenshot. In this case, even if no node is selected, the user may still see the icon to open the mask editor when hovering the mouse over the image. However, because no node is currently selected, the mask editor cannot be opened in this situation, since the mask editor requires accessing the selected node.
christian-byrne
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
|
In follow up (when we have time), we can do: Add something to keep in sync with watch(
() => props.imageUrls,
(newUrls) => {
const node = app.graph.getNodeById(props.nodeId)
if (!node) return
node.images = [currentImageEl] as string[]
},
{ deep: true, flush: 'post' }
)Then we can just remove this, which might be a little bit confusingly named currently: ComfyUI_frontend/src/stores/imagePreviewStore.ts Lines 334 to 352 in bee09d5
|
## Summary fix mask editor issues on vueNodes ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5953-fix-mask-editor-bug-under-vueNodes-2856d73d3650810aa8a2e1a94c4d97a6) by [Unito](https://www.unito.io)
Summary
fix mask editor issues on vueNodes
┆Issue is synchronized with this Notion page by Unito